神经网络训练详解

使用多层神经网络拟合 y = x² 函数

一、神经网络训练过程

神经网络的训练是一个迭代优化的过程,目标是找到一组最优的权重参数,使得网络的预测输出尽可能接近真实目标值。整个过程可以分为三个核心步骤:

  1. 前向传播 (Forward Propagation)
    输入数据通过网络各层依次计算,每一层执行线性变换和激活函数,最终得到预测输出。
    Y = f(X · W + b)
  2. 计算损失 (Loss Calculation)
    使用损失函数衡量预测值与真实值之间的差距。本例使用均方误差 (MSE):
    Loss = 1 n × Σ(YpredYtarget
  3. 反向传播 (Backpropagation)
    根据链式法则,计算损失对每个参数的梯度,然后使用梯度下降更新权重:
    W = Wlr × Loss W

梯度计算详解

以单层线性网络为例,推导 ∂Loss/∂W 的计算过程:

已知:

  • 前向传播:Y = X · W + b
  • 损失函数:Loss = 1n Σ(YYtarget

Step 1:计算 Loss 对 Y 的梯度

Loss Y = 2 n (YYtarget)

Step 2:应用链式法则,计算 Loss 对 W 的梯度

由于 Y = X · W + b,所以 ∂Y/∂W = XT

Loss W = Loss Y · Y W = XT · 2 n (YYtarget)

Step 3:计算 Loss 对 b 的梯度

由于 Y = X·W + b,对 b 求偏导时,X·W 是常数:

Y b = ∂(X·W + b) b = 0 + 1 = 1

直观理解:b 增加 1,Y 就增加 1,变化率是 1:1,所以导数 = 1

因此,梯度为各样本梯度之和:

Loss b = Σ 2 n (YYtarget)

Step 4:更新参数

Wnew = Wlr × Loss W bnew = blr × Loss b

多层网络:对于多层神经网络,梯度通过链式法则从输出层逐层向输入层传递,每层依次计算。这就是"反向传播"名称的由来。

训练循环:以上三个步骤构成一个 epoch(训练轮次),重复执行直到损失收敛到满意的水平。

二、完整源代码

bp-x2.py Python
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

# ============== 可修改参数 ==============
# 网络结构:每个数字代表该层的神经元数量
HIDDEN_LAYERS = [64, 32]

# 激活函数选择: 'relu', 'tanh', 'sigmoid', 'leaky_relu', 'elu'
ACTIVATION = 'relu'

# 训练参数
lr = 0.01                    # 学习率
epochs = 2000                # 最大训练轮数
num_samples = 200            # 训练样本数量
early_stop_patience = 50     # 早停耐心值
early_stop_threshold = 0.01  # 早停阈值
# ========================================

# 激活函数映射
ACTIVATION_MAP = {
    'relu': nn.ReLU(),
    'tanh': nn.Tanh(),
    'sigmoid': nn.Sigmoid(),
    'leaky_relu': nn.LeakyReLU(0.1),
    'elu': nn.ELU(),
}

def build_network(input_dim, hidden_layers, output_dim, activation_name):
    """动态构建多层神经网络"""
    layers = []
    prev_dim = input_dim
    activation = ACTIVATION_MAP.get(activation_name, nn.ReLU())

    # 添加隐藏层
    for hidden_dim in hidden_layers:
        layers.append(nn.Linear(prev_dim, hidden_dim))
        layers.append(activation)
        prev_dim = hidden_dim

    # 输出层(不加激活函数,因为是回归任务)
    layers.append(nn.Linear(prev_dim, output_dim))
    return nn.Sequential(*layers)

# 生成训练数据: y = x²
X = torch.linspace(-5, 5, num_samples).reshape(-1, 1)
Y_target = X ** 2

# 构建网络
model = build_network(1, HIDDEN_LAYERS, 1, ACTIVATION)

# 优化器和损失函数
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
loss_fn = nn.MSELoss()

# 训练循环
loss_history = []
best_loss = float('inf')
patience_counter = 0

for epoch in range(epochs):
    # 1. 前向传播
    Y_pred = model(X)
    loss = loss_fn(Y_pred, Y_target)

    # 2. 反向传播
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    # 记录和早停检查
    current_loss = loss.item()
    loss_history.append(current_loss)

    if current_loss < best_loss:
        best_loss = current_loss
        patience_counter = 0
    else:
        patience_counter += 1

    if patience_counter >= early_stop_patience and current_loss < early_stop_threshold:
        print(f"早停触发!")
        break

print(f"训练完成! 最终 Loss: {loss_history[-1]:.6f}")

三、训练输出结果

==================================================
网络结构:
Sequential(
  (0): Linear(in_features=1, out_features=64, bias=True)
  (1): ReLU()
  (2): Linear(in_features=64, out_features=32, bias=True)
  (3): ReLU()
  (4): Linear(in_features=32, out_features=1, bias=True)
)
==================================================
隐藏层: [64, 32]
激活函数: relu
学习率: 0.01, 训练轮数: 2000
==================================================
开始训练...

Epoch   100 | Loss: 1.653280 | Best: 1.653280
Epoch   200 | Loss: 0.249146 | Best: 0.249146
Epoch   300 | Loss: 0.056769 | Best: 0.056769
Epoch   400 | Loss: 0.018616 | Best: 0.018616
Epoch   500 | Loss: 0.008368 | Best: 0.008368
Epoch   600 | Loss: 0.004815 | Best: 0.004815
Epoch   700 | Loss: 0.003291 | Best: 0.003291
Epoch   800 | Loss: 0.002482 | Best: 0.002482
Epoch   900 | Loss: 0.001994 | Best: 0.001994
Epoch  1000 | Loss: 0.001664 | Best: 0.001664
Epoch  1100 | Loss: 0.001435 | Best: 0.001435
Epoch  1200 | Loss: 0.001262 | Best: 0.001262
Epoch  1300 | Loss: 0.001136 | Best: 0.001136
Epoch  1400 | Loss: 0.001030 | Best: 0.001030
Epoch  1500 | Loss: 0.000943 | Best: 0.000943
Epoch  1600 | Loss: 0.000875 | Best: 0.000875
Epoch  1700 | Loss: 0.000819 | Best: 0.000819
Epoch  1800 | Loss: 0.000770 | Best: 0.000770
Epoch  1900 | Loss: 0.000726 | Best: 0.000726
Epoch  2000 | Loss: 0.000685 | Best: 0.000685

==================================================
训练完成! 最佳 Loss: 0.000685 (Epoch 2000)
最终 Loss: 0.000685
==================================================
训练结果图

图:左侧为拟合效果对比,右侧为 Loss 下降曲线

四、结果分析

4.1 拟合效果

拟合成功!红色虚线(神经网络预测)与蓝色实线(y = x²)几乎完全重合,说明网络成功学习到了二次函数的映射关系。最终 MSE Loss 低至 0.000685,预测误差极小。

4.2 Loss 曲线分析

从 Loss 曲线可以观察到:

五、代码实现详解

5.1 网络架构

层级 类型 输入维度 输出维度 说明
第1层 Linear + ReLU 1 64 扩展特征维度
第2层 Linear + ReLU 64 32 特征压缩
输出层 Linear 32 1 回归输出(无激活)

为什么输出层不加激活函数?

这是回归任务的关键设计。激活函数会限制输出范围,而回归任务需要输出任意值:

激活函数 输出范围 问题
ReLU [0, +∞) 无法输出负数
Sigmoid (0, 1) 只能输出 0~1
Tanh (-1, 1) 只能输出 -1~1
无激活 (-∞, +∞) 任意值 ✓

以本例 y = x² 为例:

  • x 范围:[-5, 5]
  • y 范围:[0, 25]

如果输出层加了 Tanh,最大只能输出 1,永远无法拟合到 25!

不同任务的输出层设计

# 回归任务(预测连续值):无激活
nn.Linear(32, 1)  # 直接输出

# 二分类任务:Sigmoid(输出概率 0~1)
nn.Linear(32, 1)
nn.Sigmoid()

# 多分类任务:Softmax(输出概率分布)
nn.Linear(32, 10)  # 10个类别
nn.Softmax(dim=1)

简单理解:

  • 隐藏层:加激活函数 → 学习非线性特征
  • 输出层:根据任务决定
    • 回归 → 不加(要输出真实数值)
    • 分类 → 加 Sigmoid/Softmax(要输出概率)

隐藏层为什么必须加激活函数?

不是"必须",但不加就失去了意义

如果不加激活函数会怎样?

假设两层线性变换,不加激活函数:

Y = X · W₁ · W₂ + b

这等价于:

Y = X · W + b    (其中 W = W₁ · W₂)

多层线性变换 = 单层线性变换,网络再深也没用!

数学证明:

# 两层无激活
layer1: Y₁ = X · W₁ + b₁
layer2: Y₂ = Y₁ · W₂ + b₂

# 展开
Y₂ = (X · W₁ + b₁) · W₂ + b₂
   = X · W₁ · W₂ + b₁ · W₂ + b₂
   = X · W' + b'    # 仍是线性!

激活函数的作用:

# 有激活函数
layer1: Y₁ = ReLU(X · W₁ + b₁)  # 非线性变换
layer2: Y₂ = Y₁ · W₂ + b₂

# 无法合并!网络获得了表达非线性的能力

对比实验:

配置 能否拟合 y=x²
1层无激活 ❌ 只能拟合直线
10层无激活 ❌ 仍只能拟合直线
2层有ReLU ✓ 可以拟合曲线
结论:激活函数 = 引入非线性 = 让深度网络有意义。没有激活函数,神经网络就是个"昂贵的线性回归"。

5.2 关键组件

常用优化器详解

AI训练中常用的优化器可分为三大类:

1. 基础优化器

  • SGD(随机梯度下降):最基础的优化器,可加入 momentum 加速收敛
  • SGD + Momentum:引入动量,帮助跳出局部最优

2. 自适应学习率优化器

  • AdaGrad:对稀疏特征友好,但学习率会单调递减
  • RMSprop:解决 AdaGrad 学习率过快衰减的问题
  • Adam:结合 Momentum 和 RMSprop,目前最常用
  • AdamW:Adam 的改进版,修正了权重衰减的实现方式,大模型训练中广泛使用
  • Adafactor:节省内存的变体,适合超大模型

3. 新型优化器

  • LAMB:适合大 batch 训练
  • Lion:Google 提出,内存占用更低
  • Sophia:二阶优化器,训练效率更高

PyTorch 中使用优化器

方式1:别名导入(推荐,更简洁)

import torch.optim as optim

# SGD + Momentum
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

# Adam(通用首选)
optimizer = optim.Adam(model.parameters(), lr=1e-4)

# AdamW(大模型训练主流选择)
optimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)

方式2:直接使用完整路径

import torch

# 效果与方式1完全相同
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)

两种写法效果完全一样,只是风格不同。大部分优化器只需设置学习率、权重衰减等参数即可。一些新型优化器如 Lion 可能需要额外安装库(如 pip install lion-pytorch)。

优化器对比表

优化器 全称 优点 缺点 使用场景
SGD Stochastic Gradient Descent 实现简单;泛化性能好;内存占用小 收敛慢;容易陷入局部最优;对学习率敏感 小规模模型;对泛化要求高的任务
SGD + Momentum SGD with Momentum 加速收敛;减少震荡;有助于跳出局部最优 引入额外超参数;仍需仔细调参 CNN图像分类;需要稳定泛化的任务
AdaGrad Adaptive Gradient Algorithm 自动调整学习率;对稀疏特征友好 学习率单调递减,后期几乎停止学习 NLP中的词嵌入;稀疏数据场景
RMSprop Root Mean Square Propagation 解决学习率过快衰减;适合非平稳目标 仍需手动设置初始学习率 RNN/LSTM训练;在线学习
Adam Adaptive Moment Estimation 收敛快;对超参数不敏感;自适应学习率 泛化性能可能不如SGD;内存占用较大 通用首选;快速原型验证;Transformer
AdamW Adam with Decoupled Weight Decay 正确实现权重衰减;泛化更好;训练稳定 内存占用同Adam 大语言模型训练(GPT、BERT等);当前主流
Adafactor Adaptive Factor 大幅节省内存;适合超大模型 可能不如Adam稳定 显存受限时训练超大模型(如T5)
LAMB Layer-wise Adaptive Moments for Batch training 支持超大batch训练;收敛快 小batch效果不明显 分布式大规模训练;BERT预训练
Lion Evolved Sign Momentum 内存占用比Adam少一半;更新规则简单 较新,实践经验较少;对学习率更敏感 视觉模型(ViT);追求内存效率
Sophia Second-order Clipped Stochastic Optimization 二阶信息加速收敛;训练步数更少 计算开销略高;实现复杂 LLM预训练;追求训练效率

优化器选择建议

场景 推荐优化器
快速实验 / 原型开发 Adam
大语言模型预训练 AdamW
显存紧张的大模型 AdafactorLion
追求最佳泛化(CV任务) SGD + Momentum
超大 batch 分布式训练 LAMB

六、训练参数设置指南

学习率 (lr = 0.01)

控制每次参数更新的步长大小。

推荐范围:0.001 ~ 0.1

训练轮数 (epochs = 2000)

完整遍历训练数据的次数。

推荐范围:500 ~ 5000

样本数量 (num_samples = 200)

训练数据点的数量。

推荐范围:100 ~ 1000

6.1 学习率 (lr) 设置不当的问题

lr 过大 (如 lr = 1.0)

  • 现象:Loss 剧烈震荡,无法收敛,甚至变成 NaN
  • 原因:参数更新步长太大,直接"跨过"最优解,在最优点附近来回震荡

lr 过小 (如 lr = 0.00001)

  • 现象:Loss 下降极慢,需要大量 epoch 才能收敛
  • 原因:每次更新的步长太小,像"蜗牛爬行"一样缓慢接近最优解

6.2 训练轮数 (epochs) 设置不当的问题

epochs 过少 (如 epochs = 50)

  • 现象:欠拟合,Loss 还很高就停止了,预测曲线与目标偏差大
  • 原因:网络还没学够,参数还未收敛到最优值

epochs 过多 (如 epochs = 100000)

  • 现象:过拟合,在训练数据上表现好,但泛化能力下降;训练时间过长
  • 原因:网络开始"记忆"训练数据的噪声,而非学习真实规律

6.3 样本数量 (num_samples) 设置不当的问题

样本过少 (如 num_samples = 10)

  • 现象:在采样点上拟合好,但在未见过的点上预测差
  • 原因:数据太稀疏,网络无法学习到完整的函数形态

样本过多 (如 num_samples = 100000)

  • 现象:训练速度慢,内存占用大
  • 原因:对于简单函数,过多样本是冗余的,增加计算开销

6.4 参数调优建议

最佳实践

  1. 从默认值开始:lr=0.01, epochs=1000, num_samples=200
  2. 观察 Loss 曲线:
    • 如果震荡 → 降低 lr
    • 如果下降太慢 → 提高 lr 或增加 epochs
    • 如果很快就不再下降 → 可能需要更复杂的网络
  3. 使用早停机制:自动找到最佳停止点,避免过拟合
  4. 分阶段调参:先调 lr,再调 epochs,最后调网络结构

6.5 不同参数组合效果对比

参数组合 最终 Loss 效果评价
lr=0.01, epochs=2000 0.000685 ✓ 优秀
lr=0.001, epochs=2000 ~0.01 △ 收敛慢
lr=0.1, epochs=2000 ~0.001 ✓ 可接受(可能震荡)
lr=1.0, epochs=2000 NaN ✗ 发散